Análisis Exploratorio de Datos. La Maratón de Tokyo 2025.

Autores/as

Alba Martínez de la Hermosa

Alonso González Romero

Daniel López Paredes

Fecha de publicación

16 de octubre de 2025

Resumen
En el presente Análisis Exploratorio de Datos (EDA) se busca comprender los resultados que se han obtenido de la ‘Major Marathon’ de Tokyo 2025. En la etapa de limpieza se comprobará la coherencia de los datos para seguidamente poder estudiar las variables por separado (Análisis Univariante), crear otras nuevas y conocer cómo se relacionan entre ellas (Análisis Bivariante). De los resultados se podrán extraer conclusiones y plantear nuevas preguntas e ideas que sirvan de base para futuros análisis.
Palabras clave

maratón, análisis de datos, Tokyo

1 Importación de librerías.

# Importación de todas las librerías usadas durante el informe y breve descripcion
library(readr) # Libreria para poder importar los datos desde un csv.
library(ggplot2) # Libreria para poder hacer gráficos.
library(DT) # Libreria para poder visualizar dataframes en qmd de manera interactiva.
library(hms) # Libreria para poder tratar datos referentes a horas, minutos y segundos.
library(dplyr) # Libreria para poder manipular dataframes.
library(tidyr) # Libreria para poder transformar dataframes.
library(e1071) # para skewness() y kurtosis()
library(stringr) # Para la limpieza de nombres del Dataframe.
library(knitr) # Para tablas bien formateadas
library(viridis) # Paletas de color para los gráficos.
library(plotly) # Para gráficas de mapas.
library(gapminder) # Usaremos este paquete para la lista de países estándar
library(patchwork) # para combinar gráficos ggplot2
library(skimr)
library(naniar)
library(grid) # para funciones de gráficos adicionales

2 Introducción

El maratón de Tokyo 2025 se celebró, en su 18ª edición, el domingo 2 de marzo de 2025. Esta edición forma parte de los World Marathon Majors y abrió la temporada 2025 de los grandes maratones internacionales. El recorrido atraviesa distintos puntos icónicos de la ciudad de Tokyo, estando su inicio frente al edificio del Gobierno Metropolitano y la línea de meta cerca de la estación Tokyo/Gyoko-dori Avenue. El maratón de Tokyo es un evento de gran participación, tanto de atletas de élite como de aficionados.

En el presente documento se realizará un análisis exploratorio de datos (EDA) con el objetivo de extraer información relevante acerca de los resultados de los corredores que participaron en dicha prueba.

3 Presentación y Descripción del Dataset

Los resultados del maratón de Tokyo han sido extraídos a través del siguiente enlace. La extracción se ha realizado a través de técnicas de Web Scrapping con el objetivo de poder obtener datos relevantes referentes a los resultados de los corredores participantes.

3.1 Importación del Dataset

A continuación, se realiza la correspondiente importación de los datos extraídos:

resultadosTokyo2025 <- read_csv(
  "data/Maraton_Tokyo/marathon_tokyo_results_2025.csv",
  col_types = cols(
    BIB = col_integer(),
    Nombre = col_character(),
    Nacionalidad = col_character(),
    Genero = col_character(),
    Edad = col_integer(),
    tiempo_oficial = col_time(format = "%H:%M:%S"),
    parcial_5km = col_time(format = "%H:%M:%S"),
    parcial_10km = col_time(format = "%H:%M:%S"),
    parcial_15km = col_time(format = "%H:%M:%S"),
    parcial_20km = col_time(format = "%H:%M:%S"),
    medio_maraton = col_time(format = "%H:%M:%S"),
    parcial_25km = col_time(format = "%H:%M:%S"),
    parcial_30km = col_time(format = "%H:%M:%S"),
    parcial_35km = col_time(format = "%H:%M:%S"),
    parcial_40km = col_time(format = "%H:%M:%S")
  ),
  quote = "\""
)

# Transformacion a formato dataframe.
resultadosTokyo2025 <- as.data.frame(resultadosTokyo2025)

3.2 Descripción del Dataset

El dataframe importado, resultadosTokyo2025, consta de las siguientes variables:

Variable Tipo Descripción Unidades
BIB Numérica/Entero Número de dorsal asignado al corredor, valor único Número entero
Nombre Cadena de texto Nombre y apellidos del corredor Texto
Nacionalidad Cadena de texto País de procedencia del corredor Texto
Genero Categórica Género del corredor Texto
Edad Numérica/Entero Edad del corredor Años
tiempo_oficial Tiempo Tiempo total oficial de la maratón (gross time) hh:mm:ss
parcial_5km Tiempo Tiempo de paso en el km 5 hh:mm:ss
parcial_10km Tiempo Tiempo de paso en el km 10 hh:mm:ss
parcial_15km Tiempo Tiempo de paso en el km 15 hh:mm:ss
parcial_20km Tiempo Tiempo de paso en el km 20 hh:mm:ss
medio_maraton Tiempo Tiempo al paso del medio maratón (21,097 km) hh:mm:ss
parcial_25km Tiempo Tiempo de paso en el km 25 hh:mm:ss
parcial_30km Tiempo Tiempo de paso en el km 30 hh:mm:ss
parcial_35km Tiempo Tiempo de paso en el km 35 hh:mm:ss
parcial_40km Tiempo Tiempo de paso en el km 40 hh:mm:ss

Nota: El gross time es el tiempo que tarda un corredor en terminar la maratón desde que se da el pistoletazo de salida, no desde que cruza la línea de inicio de la prueba

3.3 Lectura de una fila

Una vez que conocemos el significado de cada variable por separado, se va a proceder a la lectura de la primera fila del dataframe con el objetivo de mejorar la comprensión sobre el formato de los datos:

# Lo visualizamos con la libreria DT porque es más interactiva a la hora de generar el documento qmd.
datatable(
  resultadosTokyo2025,
  options = list(
    pageLength = 1, # cuántas filas mostrar
    scrollX = TRUE, # habilita scroll horizontal si la fila es muy ancha
    dom = 't' # solo muestra la tabla sin paginación ni búsqueda
  ),
  rownames = FALSE # quitar número de fila en la tabla
)
Warning in instance$preRenderHook(instance): It seems your data is too big for
client-side DataTables. You may consider server-side processing:
https://rstudio.github.io/DT/server.html

Se puede observar al corredor Tadese Takele, de nacionalidad Etíope, que corrió la maratón con el dorsal número 5. Tadese completó la maratón a sus 22 años con un tiempo de 2 horas, 3 minutos y 23 segundos, pudiendose observar sus tiempos de paso cada 5 kilómetros y en el punto de la media maratón.

3.4 Dimensiones del dataset

filas <- nrow(resultadosTokyo2025)
columnas <- ncol(resultadosTokyo2025)
cat(
  "El dataframe resultadosTokyo2025 contiene",
  filas,
  "filas y",
  columnas,
  "columnas."
)
El dataframe resultadosTokyo2025 contiene 36173 filas y 15 columnas.
# Elimino el número de filas y columnas con el objetivo de no sobrecargar el environment.
rm(filas, columnas)

4 Preparación y limpieza de los datos

Como se ha podido observar en la lectura Sección 3.3, el dataset presenta diversas particularidades que requieren atención antes de proceder con el análisis estadístico univariante. En primer lugar, algunas variables presentan formatos mixtos (japonés e inglés separados por “/”), lo cual dificulta la legibilidad y el procesamiento posterior. En segundo lugar, los tiempos están almacenados en formato HMS (horas:minutos:segundos), lo que complica operaciones aritméticas y comparaciones.

Esta sección se procederá a la preparación del dataset mediante:

  1. Detección y tratamiento de valores nulos e inconsistencias: identificación de datos faltantes, duplicados y valores atípicos que comprometan la calidad del análisis.

  2. Estandarización de formatos: homogeneización de variables textuales (nombres, nacionalidades) para mejorar la legibilidad, facilitar una posible portabilidad del código (permitiendo cruzar datos con otros maratones) y habilitar nuevas formas de análisis posteriores.

  3. Transformación y creación de variables derivadas: conversión de tiempos a formato numérico (segundos), cálculo de ritmos parciales cada 5 km, y categorización de corredores según nivel de rendimiento (élite, semi-profesional, amateur).

Estas transformaciones permitirán un análisis univariante y multivariante más robusto y facilitarán la interpretación de patrones de rendimiento en la carrera.

4.1 Detección de Problemas de Calidad

En la actual sección se procederá a realizar un análisis de los posibles valores tomados por cada variable del conjunto de datos con el objetivo de detectar posibles irregularidades en los datos, ya sea a través de datos nulos (ya detectados u ocultos), duplicados, inconsistencias o valores atípicos. Para ello, vamos a comenzar con un vistazo a los datos agrupado por cada variable:

# Resumen global de todas las variables.
skimr::skim(resultadosTokyo2025)
Data summary
Name resultadosTokyo2025
Number of rows 36173
Number of columns 15
_______________________
Column type frequency:
character 3
difftime 10
numeric 2
________________________
Group variables None

Variable type: character

skim_variable n_missing complete_rate min max empty n_unique whitespace
Nombre 0 1 11 81 0 35916 0
Nacionalidad 0 1 1 24 0 127 0
Genero 0 1 6 19 0 3 0

Variable type: difftime

skim_variable n_missing complete_rate min max median n_unique
tiempo_oficial 0 1 7403 secs 25218 secs 04:36:13.0 13775
parcial_5km 10 1 865 secs 3855 secs 00:28:27.0 1583
parcial_10km 7 1 1735 secs 6491 secs 00:56:39.0 2980
parcial_15km 9 1 2610 secs 8654 secs 01:25:07.0 4407
parcial_20km 9 1 3487 secs 11581 secs 01:54:24.0 5954
medio_maraton 4 1 3678 secs 12197 secs 02:00:56.0 6299
parcial_25km 14 1 4363 secs 14527 secs 02:24:24.0 7523
parcial_30km 8 1 5242 secs 17334 secs 02:56:54.0 9152
parcial_35km 5 1 6133 secs 20184 secs 03:31:24.0 10617
parcial_40km 11 1 7019 secs 23424 secs 04:08:14.5 12102

Variable type: numeric

skim_variable n_missing complete_rate mean sd p0 p25 p50 p75 p100 hist
BIB 0 1 29348.23 11716.42 1 19748 29417 39083 91050 ▃▇▆▁▁
Edad 0 1 46.59 11.07 19 38 47 55 84 ▂▆▇▃▁
# Resumen específico: Variable BIB. 
cat("La variable BIB contiene", n_distinct(resultadosTokyo2025$BIB), "valores distintos")
La variable BIB contiene 36173 valores distintos
# Resumen específico: Variable EDAD. 
cat("La variable Edad tiene los siguientes valores distintos: ", unique(resultadosTokyo2025$Genero))
La variable Edad tiene los siguientes valores distintos:  男性/Men 女性/Women ノンバイナリー/Non-binary
# Resumen específico: Variable Nacionalidad. 

cat("La variable nacionalidad toma los siguientes valores: ", unique(resultadosTokyo2025$Nacionalidad))
La variable nacionalidad toma los siguientes valores:  ETHIOPIA KENYA SWEDEN UGANDA 日本 PR OF CHINA GERMANY GREAT BRITAIN & N.I. UNITED STATES FRANCE BAHRAIN CHINESE TAIPEI HONG KONG, CHINA AUSTRALIA PHILIPPINES POLAND MONGOLIA UKRAINE ESTONIA BELGIUM MALAYSIA CAMBODIA SPAIN VENEZUELA VIETNAM UZBEKISTAN SWITZERLAND SINGAPORE COSTA RICA RUSSIA ITALY BRAZIL NEPAL NEW ZEALAND KOREA PORTUGAL CANADA CHILE ARGENTINA THAILAND SOUTH AFRICA NETHERLANDS MEXICO HONDURAS COLOMBIA NORWAY LITHUANIA CZECH REPUBLIC ISRAEL DOMINICAN REPUBLIC FINLAND GUATEMALA GREECE KAZAKHSTAN PERU MACAO HUNGARY DENMARK CROATIA IRELAND INDONESIA - ECUADOR AUSTRIA ROMANIA PUERTO RICO UNITED ARAB EMIRATES PANAMA BURUNDI SLOVENIA EGYPT BARBADOS BULGARIA BRUNEI BERMUDA NICARAGUA BELARUS DPR OF KOREA MOLDOVA INDIA EL SALVADOR LATVIA SERBIA PALESTINE F Y REP. OF MACEDONIA VIRGIN ISLANDS TURKEY SLOVAK REPUBLIC URUGUAY ICELAND AFGHANISTAN ALGERIA NIGERIA PAKISTAN MOROCCO PARAGUAY TANZANIA MALDIVES ARMENIA SAINT KITTS AND NEVIS TUNISIA JORDAN CYPRUS BAHAMAS CAYMAN ISLANDS ZIMBABWE GUAM MALTA LUXEMBOURG GUYANA MAURITIUS SAUDI ARABIA BHUTAN LIBYA SUDAN REPUBLIC Of YEMEN BOLIVIA QATAR BENIN BELIZE ZAMBIA JAMAICA ISLAMIC REPUBLIC OF IRAN TURKMENISTAN SAINT LUCIA MYANMAR LAOS

De este resumen podemos resaltar que:

  • Existen 35916 nombres únicos en el conjunto de datos, pero 36173 corredores, habrá que analizar posibles duplicados.
  • En los parciales, existen datos faltantes NA.
  • En la variable Nacionalidad, existen valores de tipo - que determinan datos faltantes ocultos.

Tras este primer análisis global de las variables, vamos a proceder a identificar concretamente los problemas de calidad y tras ello, a tratarlos.

4.1.1 Duplicados e Inconsistencias

Comenzamos realizando un análisis de posibles datos duplicados. Se ha podido observar en el análisis inicial que teníamos datos duplicados en las variables:

  • Nombre
  • Parciales.

Cabe destacar que es normal que, al haber tantos corredores, existan datos duplicados en los parciales, ya que los corredores podían encontrarse en paralelo (o con diferencia menor a un minuto) al paso de cada sector y por ello tienen en esa variable el mismo dato.

Respecto a los duplicados en la variable Nombre, vamos a proceder a hacer un pequeño análisis con el objetivo de identificar el por qué de ello. Vamos a comenzar visualizando en una tabla aquellas filas identificadas con valores duplicados en la variable Nombre:

nombres_duplicados <- resultadosTokyo2025 %>%
  group_by(Nombre) %>%
  filter(n() > 1) %>%
  ungroup() %>%
  arrange(Nombre)

# Dimensiones
cat("Total de filas con nombres duplicados:", nrow(nombres_duplicados), "\n")
Total de filas con nombres duplicados: 488 
cat("Cantidad de nombres únicos duplicados:", n_distinct(nombres_duplicados$Nombre), "\n")
Cantidad de nombres únicos duplicados: 231 
datatable(
  nombres_duplicados,
  options = list(
    pageLength = 10,      # Muestra 10 filas por página
    scrollX = TRUE,       # Habilita el scroll horizontal
    dom = 'tip'           # Muestra la tabla, información ("Showing...") y paginación
  ),
  rownames = FALSE      # Quita los números de fila
)

De esta tabla y resumen podemos observar lo siguiente:

  • Existen valores nulos ocultos identificados cómo (** ** / *** ***)
  • Existen nombres duplicados entre los corredores pero, tanto el BIB como los tiempos parciales son completamente distintos. Por lo tanto son datos consistentes.

Vamos a proceder a determinar como NA’s reconocidos por el lenguaje estos valores nulos ocultos:

# Eliminamos los datos recién creados: 
rm(nombres_duplicados)
# Modificamos el df inicial añadiendo estos datos faltantes. 
resultadosTokyo2025 <- resultadosTokyo2025 %>% mutate(Nombre = na_if(Nombre, "*** *** / *** ***"))

Una vez identificados los datos nulos ocultos de la variable Nombre, vamos a continuar con la variable Nacionalidad, como bien hemos detectado en Sección 4.1, esta variable toma valores “-” que corresponden a corredores a los que les falta el dato de la nacionalidad. Vamos a modificar este valor para tener identificados los datos nulos correspondientes a esta variable:

resultadosTokyo2025<- resultadosTokyo2025 %>% mutate(Nacionalidad = na_if(Nacionalidad, "-"))

4.1.2 Valores Atípicos Lógicos

Tras una revisión preliminar de las variables numéricas en la sección Sección 4.1, los datos de tiempos finales y parciales no presentan valores evidentemente imposibles, como registros negativos o duraciones extrañas.

Sin embargo, es crucial verificar la integridad lógica secuencial de los datos. Por ello, esta sección se centrará en detectar parciales inconsecuentes. Este tipo de anomalía ocurre cuando el tiempo acumulado en un punto de control es erróneamente menor que el del punto de control anterior (es decir, tiempo_parcial_X+1 < tiempo_parcial_X), lo cual es físicamente imposible y señala un error en la captura o registro de los datos.

inconsistentes_parciales <- resultadosTokyo2025 %>%
  filter(
    parcial_5km >= parcial_10km |
    parcial_10km >= parcial_15km |
    parcial_15km >= parcial_20km |
    parcial_20km >= medio_maraton |
    medio_maraton >= parcial_25km |
    parcial_25km >= parcial_30km |
    parcial_30km >= parcial_35km |
    parcial_35km >= parcial_40km |
    parcial_40km >= tiempo_oficial
  )

dim(inconsistentes_parciales)
[1]  0 15

Observamos que el filtrado recibe 0 filas que cumplan que un parcial posterior tenga menor tiempo acumulado que uno anterior, por lo tanto no existen valores atípicos lógicos sobre estas variables.

rm(inconsistentes_parciales)

4.1.3 Valores Nulos y Faltantes

En esta subsección se realizará un análisis de los valores nulos y faltantes que existen en el conjunto de datos con el objetivo de identificar posibles patrones y establecer una estrategia para tratarlos.

Para ello, comenzamos realizando un análisis de completitud por variable:

naniar::miss_var_summary(resultadosTokyo2025)
# A tibble: 15 × 3
   variable       n_miss pct_miss
   <chr>           <int>    <num>
 1 parcial_25km       14   0.0387
 2 parcial_40km       11   0.0304
 3 parcial_5km        10   0.0276
 4 parcial_15km        9   0.0249
 5 parcial_20km        9   0.0249
 6 parcial_30km        8   0.0221
 7 parcial_10km        7   0.0194
 8 Nombre              5   0.0138
 9 parcial_35km        5   0.0138
10 Nacionalidad        4   0.0111
11 medio_maraton       4   0.0111
12 BIB                 0   0     
13 Genero              0   0     
14 Edad                0   0     
15 tiempo_oficial      0   0     

Se puede observar que tenemos un porcentaje total de datos nulos extremadamente bajo en cada variable. Se pueden observar que los datos nulos se agrupan en:

  1. Datos nulos en la variable Nombre
  2. Datos nulos en la variable Nacionalidad
  3. Datos nulos en las variables referentes a Parciales

En los dos primeros casos, se ha decidido no realizar ninguna imputación ni tampoco eliminar las filas afectadas. Estos registros no se considerarán a la hora de realizar estudios univariantes o bivariantes que las involucren, manteniendo así información de especial relevancia en las demás variables.

Respecto al último caso, vamos a realizar un estudio para entender qué forma tienen los datos nulos asociados a los Parciales, para ello, comenzamos visualizando las filas con algún NA en estas filas:

# Definimos en un vector las columnas de parciales: 
cols_tiempo <- c("parcial_5km", "parcial_10km", "parcial_15km", "parcial_20km", "medio_maraton", "parcial_25km", "parcial_30km", "parcial_35km", "parcial_40km")

# Definimos las distancias asociadas a cada columna de parciales: 
dist <- c(5, 10, 15, 20, 21.0975, 25, 30, 35, 40)

#Filas con NA
filas_con_na <- resultadosTokyo2025[ rowSums(is.na(resultadosTokyo2025[cols_tiempo])) > 0, ]

# Mostramos el dataframe formado por todas las filas que tienen al menos un valor faltante: 
datatable(
  filas_con_na,
  options = list(
    pageLength = 10,      # Muestra 10 filas por página
    scrollX = TRUE,       # Habilita el scroll horizontal
    dom = 'tip'           # Muestra la tabla, información ("Showing...") y paginación
  ),
  rownames = FALSE      # Quita los números de fila
)

Antes de seguir trabajando sobre el dataframe, es conveniente hacer una copia para trabajar sobre la copia sin tocar los datos originales por los erroes que pueda haber.

df_trabajo <- resultadosTokyo2025

Tras ello, vamos a visualizar, de cada fila, cuántos valores nulos en total tiene:

filas_con_na <- df_trabajo[rowSums(is.na(df_trabajo[, cols_tiempo])) > 0, ]
filas_con_na$n_na <- rowSums(is.na(filas_con_na[, cols_tiempo]))

unique(filas_con_na$n_na)
[1] 9 1 3 8 2 5

Por lo que se puede observar, el número de parciales faltantes puede ser 1, 2, 3, 5, 8 ó 9. Para finalizar el estudio de la forma de los valores nulos y faltantes de nuestro conjunto de datos, vamos a visualizar de todas estas filas, el número de parciales faltantes consecutivos, ya que será de vital importancia posterior.

filas_con_na$max_na_consec <- sapply(1:nrow(filas_con_na), function(i) {
  elems <- unlist(filas_con_na[i, cols_tiempo])
  na_vec <- is.na(elems)
  if(all(!na_vec)) return(0)
  max(rle(na_vec)$lengths[rle(na_vec)$values == TRUE])
})

unique(filas_con_na$max_na_consec)
[1] 9 1 2 4 3

Podemos observar viendo los valores que toma esta variable que, el número de parciales consecutivos faltantes puede ser 1,2,3,4 o 9.

4.2 Limpieza y Corrección de Datos

En esta sección se realizará la correspondiente limpieza y correción de datos.

4.2.1 Tratamiento de Valores Nulos

Vamos a realizar el correspondiente tratamiento de los valores nulos, como bien hemos adelantado en la Sección 4.1.3, los datos faltantes correspondientes a las variables de Nombre y Nacionalidad ni se imputarán ni se eliminarán dichas filas.

Respecto a la estrategia para los parciales, cabe destacar que ninguno de las filas corresponde a abandonos o descalificaciones ya que sí que existe su tiempo final al paso por la meta. Se ha decidido tratar los datos nulos de la siguiente manera:

  • Si una fila tiene 2 o más parciales sin tiempo registrado consecutivos, no se considerará imputable debido a que supone un desconocimiento de 15 kilómetros. Asumir un ritmo constante durante 15 kilómetros en una disciplina de duración tan larga como la maratón en atletas amateur (son los que tienen parciales sin registrar) es irreal.

Por lo tanto, de las 43 filas con datos nulos que teníamos, vamos a imputar las siguientes:

filas_con_na$clasificacion <- ifelse(
  filas_con_na$max_na_consec >= 2,
  "descartar",
  "imputable"
)
filas_con_na$descarte <- filas_con_na$clasificacion == "descartar"
filas_para_imputar <- filas_con_na[filas_con_na$descarte == FALSE, ]
datatable(
  filas_para_imputar,
  options = list(
    pageLength = 10,      # Muestra 10 filas por página
    scrollX = TRUE,       # Habilita el scroll horizontal
    dom = 'tip'           # Muestra la tabla, información ("Showing...") y paginación
  ),
  rownames = FALSE      # Quita los números de fila
)

Es decir, se produce un descarte de 9 filas del conjunto de datos, ya que tienen más de dos parciales consecutivos con datos faltantes. Respecto a las filas que sí son imputables, hay que tener en cuenta la forma en la que lo vamos a realizar dependiendo cuál sea el parcial faltante:

  • Si el dato faltante es parcial_5km, se imputará teniendo en cuenta que el ritmo en el primer tramo de la maratón es, en promedio un 5% más rápido que el ritmo de tramos posteriores.
  • Si el dato faltante es parcial_40km, habrá que considerar que desde el parcial 35 hay 5km de distancia pero para la llegada a la meta hay 2km y 195m.
  • Si el dato faltante es media_maraton, es preciso saber que desde el parcial del kilómetro 20 hay 1km y 96m de distancia y 3km 904m hasta el kilómetro 25.
  • Si el dato faltante es cualquier otro parcial, se imputará calculando el tiempo de paso con los parciales inmediatamente anterior y posterior a él.

Vamos a comenzar por ello, descartando del conjunto de datos las filas que hemos clasificado como no imputables, lo haremos realizando un cruce por el BIB (único) y filtrando aquellas filas cuya clasificación sea “descartar”:

bibs_a_descartar <- filas_con_na %>%
  filter(clasificacion == "descartar") %>%
  select(BIB)

resultadosTokyo2025<- resultadosTokyo2025 %>%
  anti_join(bibs_a_descartar, by = "BIB")

rm(bibs_a_descartar, filas_con_na)

Una vez descartados aquellos corredores no imputables, vamos a realizar la imputación de aquellos que sí que es posible realizarla. Para ello, vamos a comenzar imputando aquellos tiempos que no sean ni parcial_5km ni parcial_40km, para ello, creamos la siguiente función que gracias a una interpolación lineal estándar, nos ayudará a calcular el tiempo que necesitamos imputar con los tiempos y distancias previo y posterior:

imputar_parciales <- function(tiempos, dist, cols_tiempo) {
  if (!inherits(tiempos, "hms")) tiempos <- hms::as_hms(tiempos)
  
  for (i in seq_along(tiempos)) {
    if (is.na(tiempos[i])) {
      
      prev_idx <- max(which(!is.na(tiempos[1:(i - 1)])), na.rm = TRUE)
      next_idx <- min(which(!is.na(tiempos[(i + 1):length(tiempos)])), na.rm = TRUE)
      if (is.finite(next_idx)) next_idx <- next_idx + i
      if (is.finite(prev_idx) && is.finite(next_idx)) {
        t_prev <- tiempos[prev_idx]
        t_next <- tiempos[next_idx]
        d_prev <- dist[prev_idx]
        d_next <- dist[next_idx]
        d_missing <- dist[i]
        tiempos[i] <- t_prev + (t_next - t_prev) * ((d_missing - d_prev) / (d_next - d_prev)) #interpolación lineal estándar
      }
    }
  }
  return(tiempos)
}

primera_imputacion <- as.data.frame(
  t(apply(filas_para_imputar[cols_tiempo], 1, imputar_parciales, dist = dist, cols_tiempo = cols_tiempo))
)
Warning in min(which(!is.na(tiempos[(i + 1):length(tiempos)])), na.rm = TRUE):
no non-missing arguments to min; returning Inf
Warning in max(which(!is.na(tiempos[1:(i - 1)])), na.rm = TRUE): no non-missing
arguments to max; returning -Inf
Warning in min(which(!is.na(tiempos[(i + 1):length(tiempos)])), na.rm = TRUE):
no non-missing arguments to min; returning Inf
Warning in min(which(!is.na(tiempos[(i + 1):length(tiempos)])), na.rm = TRUE):
no non-missing arguments to min; returning Inf
Warning in max(which(!is.na(tiempos[1:(i - 1)])), na.rm = TRUE): no non-missing
arguments to max; returning -Inf
Warning in max(which(!is.na(tiempos[1:(i - 1)])), na.rm = TRUE): no non-missing
arguments to max; returning -Inf
Warning in max(which(!is.na(tiempos[1:(i - 1)])), na.rm = TRUE): no non-missing
arguments to max; returning -Inf
Warning in min(which(!is.na(tiempos[(i + 1):length(tiempos)])), na.rm = TRUE):
no non-missing arguments to min; returning Inf
Warning in min(which(!is.na(tiempos[(i + 1):length(tiempos)])), na.rm = TRUE):
no non-missing arguments to min; returning Inf
Warning in max(which(!is.na(tiempos[1:(i - 1)])), na.rm = TRUE): no non-missing
arguments to max; returning -Inf
Warning in max(which(!is.na(tiempos[1:(i - 1)])), na.rm = TRUE): no non-missing
arguments to max; returning -Inf
Warning in min(which(!is.na(tiempos[(i + 1):length(tiempos)])), na.rm = TRUE):
no non-missing arguments to min; returning Inf
colnames(primera_imputacion) <- cols_tiempo
primera_imputacion[cols_tiempo] <-
  lapply(primera_imputacion[cols_tiempo], hms::as_hms)

Tras la imputación correcta de los parciales intermedios, añado la imputación de aquellos valores faltantes a los 5km, que se realizará calculando el ritmo medio estimando con el parcial de los 10km, y multiplicándolo por un factor salida de un 95%, es decir, que el tiempo de paso por los 5km es un 5% más rápido. Para ello:

#Añadir a primera_imputacion la imputacion de parcial_5km
idx_5 <- which(cols_tiempo == "parcial_5km")
idx_10 <- which(cols_tiempo == "parcial_10km")

filas_na_5 <- which(is.na(primera_imputacion$parcial_5km))

d_5 <- dist[idx_5]
d_10 <- dist[idx_10]

factor_salida <- 0.95  # 5% más rápida

primera_imputacion$parcial_5km[filas_na_5] <- hms::as_hms(
  (primera_imputacion$parcial_10km[filas_na_5] - hms::as_hms(0)) * factor_salida * (d_5 / d_10)
)

Para la imputación del parcial de 40km, se ha decidido usar el ritmo justamente anterior, es decir, el tiempo que tardó al paso por el parcial del 30 al 35, es el mismo del 35 al 40, por lo tanto se suma esa diferencia de tiempo para obtener el tiempo de paso por el kilómetro 40 de la prueba.

#Añadir a primera_imputacion la imputacion del parcial_40km
idx_35 <- which(cols_tiempo == "parcial_35km")
idx_40 <- which(cols_tiempo == "parcial_40km")
d_35 <- dist[idx_35]
d_40 <- dist[idx_40]

filas_na_40 <- which(is.na(primera_imputacion$parcial_40km))
primera_imputacion$parcial_40km[filas_na_40] <- hms::as_hms(
  primera_imputacion$parcial_35km[filas_na_40] +
    (primera_imputacion$parcial_35km[filas_na_40] - primera_imputacion$parcial_30km[filas_na_40]) *
    ((d_40 - d_35) / (d_35 - dist[idx_35 - 1]))
)

Tras ello, redondeamos los tiempos del formato hms:

redondear_hms <- function(x) {
  seg_total <- as.numeric(x)
  seg_total_rounded <- round(seg_total)
  hms::as_hms(seg_total_rounded)
}
primera_imputacion[cols_tiempo] <- lapply(
  primera_imputacion[cols_tiempo],
  redondear_hms
)

Por último, añadimos las imputaciones correspondientes al conjunto de datos inicial:

filas_para_imputar[rownames(filas_para_imputar), cols_tiempo] <- primera_imputacion

# Elimino las columnas creadas. 
filas_para_imputar <- filas_para_imputar[, !(names(filas_para_imputar) %in% c("n_na", "max_na_consec", "clasificacion", "descarte"))]
# Cargo los datos imputados actualizando las filas correspondientes.
resultadosTokyo2025 <- resultadosTokyo2025 %>%
  rows_update(filas_para_imputar, by = "BIB")

print("Datos faltantes imputados")
[1] "Datos faltantes imputados"
# Elimino todos los valores, datos y funciones creadas:
rm(df_trabajo, filas_para_imputar, primera_imputacion, d_5, d_10, d_35, d_40, dist, factor_salida, filas_na_5, filas_na_40, idx_5, idx_10, idx_35, idx_40, imputar_parciales, redondear_hms)

Por último, observo el análisis de completitud para ver cómo se ha quedado el conjunto de datos:

skimr::skim(resultadosTokyo2025)
Data summary
Name resultadosTokyo2025
Number of rows 36164
Number of columns 15
_______________________
Column type frequency:
character 3
difftime 10
numeric 2
________________________
Group variables None

Variable type: character

skim_variable n_missing complete_rate min max empty n_unique whitespace
Nombre 5 1 11 81 0 35906 0
Nacionalidad 4 1 2 24 0 126 0
Genero 0 1 6 19 0 3 0

Variable type: difftime

skim_variable n_missing complete_rate min max median n_unique
tiempo_oficial 0 1 7403 secs 25218 secs 16573.0 secs 13775
parcial_5km 0 1 865 secs 3855 secs 1707.5 secs 1584
parcial_10km 0 1 1735 secs 6491 secs 3399.0 secs 2980
parcial_15km 0 1 2610 secs 8654 secs 5107.0 secs 4407
parcial_20km 0 1 3487 secs 11581 secs 6864.0 secs 5954
medio_maraton 0 1 3678 secs 12197 secs 7256.0 secs 6299
parcial_25km 0 1 4363 secs 14527 secs 8664.0 secs 7523
parcial_30km 0 1 5242 secs 17334 secs 10614.0 secs 9152
parcial_35km 0 1 6133 secs 20184 secs 12684.0 secs 10616
parcial_40km 0 1 7019 secs 23424 secs 14894.0 secs 12102

Variable type: numeric

skim_variable n_missing complete_rate mean sd p0 p25 p50 p75 p100 hist
BIB 0 1 29347.79 11716.84 1 19745.75 29416.5 39084.25 91050 ▃▇▆▁▁
Edad 0 1 46.59 11.07 19 38.00 47.0 55.00 84 ▂▆▇▃▁

4.3 Estandarización de Formatos

En esta sección se realizará una estandarización de formatos en los valores de algunas columnas.

4.3.1 Género de los participantes

Debido a que los datos han sido extraídos de una web japonesa, los valores de la columna Genero presentan un formato bilingüe, con caracteres tanto japoneses como ingleses, separados por el caracter /. Para facilitar posteriores análisis y visualizaciones, se ha tomado la decisión de unificar esta variable a un único idioma, en este caso, el inglés. Una vez limpia, la convertimos a factor. Para ello:

resultadosTokyo2025 <- resultadosTokyo2025 %>%
  mutate(
    # Modificamos la columna 'Género'
    Genero = str_remove(Genero, "^.*/")
  )

resultadosTokyo2025$Genero <- as.factor(resultadosTokyo2025$Genero)

4.3.2 Nombre de los participantes

Al igual que el Genero, la variable Nombre presenta los nombres de cada participante con el mismo formato bilingüe, es por ello que también nos quedaremos con el idioma inglés, ya que esto nos abre la posibilidad de poder cruzar datos con otros maratones si en algún momento del análisis queremos hacer un análisis longitudinal de algún deportista.

# Versión compacta para reemplazar la columna original
resultadosTokyo2025 <- resultadosTokyo2025 %>%
  mutate(
    Nombre = coalesce(str_trim(str_split_i(Nombre, "/", 2)), Nombre)
  )

4.3.3 Códigos de Nacionalidad

Con el objetivo de poder definir de manera estándar y acorde a códigos para poder graficar más adelante países, se ha decidido estandarizar la variable Nacionalidad, ya que se puede observar cómo contiene carácteres especiales japonenes o incluso regiones cuyo nombre de país es distinto, es por ello que visualizamos primeramente los nombres:

conteo_paises <- resultadosTokyo2025 %>%
   count(Nacionalidad, sort = TRUE, name = "Numero_Corredores")

# Lo visualizamos con la libreria DT porque es más interactiva a la hora de generar el documento qmd. 
datatable(
  conteo_paises,
  options = list(
    pageLength = 10,
    scrollX = TRUE
  ),
  rownames = FALSE,  # Elimina los números de fila
  colnames = c("País", "Número de Corredores")
)

Se peude observar que no existen países con el nombre duplicado (es decir, varios nombres para el mismo país).

Vamos a crear una nueva variable en nuestro conjunto de datos que se relacione con el país con el nombre ya estandarizado, Pais_Estandarizado.

resultadosTokyo2025 <- resultadosTokyo2025 %>%
  mutate(
    # Primero, manejamos el caso del guion para que no interfiera
    Nacionalidad = na_if(Nacionalidad, "-"),
    
    # Ahora, creamos la nueva columna estandarizada
    Pais_Estandarizado = case_when(
      Nacionalidad == "日本" ~ "Japan",
      Nacionalidad == "PR OF CHINA" ~ "China",
      Nacionalidad == "CHINESE TAIPEI" ~ "Taiwan",
      Nacionalidad == "GREAT BRITAIN & N.I." ~ "United Kingdom",
      Nacionalidad == "HONG KONG, CHINA" ~ "Hong Kong",
      Nacionalidad == "KOREA" ~ "Korea, South",
      Nacionalidad == "DPR OF KOREA" ~ "Korea, North",
      Nacionalidad == "F Y REP. OF MACEDONIA" ~ "Macedonia",
      Nacionalidad == "SLOVAK REPUBLIC" ~ "Slovakia",
      Nacionalidad == "ISLAMIC REPUBLIC OF IRAN" ~ "Iran",
      Nacionalidad == "MACAO" ~ "Macau",
      Nacionalidad == "SAINT KITTS AND NEVIS" ~ "Saint Kitts and Nevis",
      Nacionalidad == "BAHAMAS" ~ "Bahamas, The",
      Nacionalidad == "REPUBLIC Of YEMEN" ~ "Yemen",
      Nacionalidad == "UNITED STATES" ~ "United States", # Usar el nombre completo suele ser más compatible
      
      # Para todos los demás países los ponemos en el formato minúsculas con la primera letra en mayúsculas.
      TRUE ~ str_to_title(Nacionalidad)
    )
  )

conteo_paises_limpios <- resultadosTokyo2025 %>%
  count(Pais_Estandarizado, sort = TRUE)

Observamos que, en principio, los errores están completamente corregidos. Para ver si los nuevos nombres estandarizados están correctamente, vamos a cruzar los países recién creados con un conjunto de datos muy usado para realizar luego mapas geográficos.

df <- read.csv("https://raw.githubusercontent.com/plotly/datasets/master/2014_world_gdp_with_codes.csv")

paises_validos <- df %>%
  distinct(COUNTRY) %>%
  # Renombramos la columna para que coincida con la nuestra y poder hacer el join
  rename(Pais_Estandarizado = COUNTRY)


paises_problematicos <- resultadosTokyo2025 %>%
  filter(!is.na(Pais_Estandarizado)) %>% # Ignoramos los NA que puedan existir
  distinct(Pais_Estandarizado) %>%
  anti_join(paises_validos, by = "Pais_Estandarizado")

dim(paises_problematicos)
[1] 2 1
paises_problematicos$Pais_Estandarizado
[1] "Palestine" "Myanmar"  

Se observan sólamente 2 países problemáticos (Palestine y Myanmar) por lo tanto, creamos una nueva variable llamada CODE_PLOTLY que contendrá el código de cada país según este dataframe subido:

resultadosTokyo2025 <- resultadosTokyo2025 %>%
  left_join(
    df %>% select(COUNTRY, CODE),  
    by = c("Pais_Estandarizado" = "COUNTRY")  
  )
rm(conteo_paises, conteo_paises_limpios, df, paises_problematicos, paises_validos)

4.3.4 Conversión de Tiempos

Con el objetivo de poder conseguir mejores análisis tanto univariantes como bivariantes en las variables referentes a tiempos (parciales o finales), se ha decidido convertirlas a segundos (numérico) para poder tratarlas adecuadamente de forma estadística. Cabe destacar que siempre que queramos visualizar las horas, minutos y segundos, podremos volver a convertirlas al formato adecuado.

resultadosTokyo2025 <- resultadosTokyo2025 %>%
  mutate(
    across(where(is_hms), as.numeric)
  )

4.4 Creación de Variables Derivadas

Con el objetivo de lograr una mayor comprensión del conjunto de datos inicial a través de los posteriores estudios, se ha tomado la decisión de crear algunas nuevas variables.

4.4.1 Ritmos Parciales

Se van a crear nuevas variables para determinar el ritmo (min/km) de los corredores al paso de cada uno de los parciales de la prueba. Además se creará una variable con el ritmo promedio basado en el tiempo_final.

Para ello:

# Creacion de la variable ritmo_oficial: 
resultadosTokyo2025 <- resultadosTokyo2025 %>%
  mutate(
    ritmo_oficial = hms(seconds = round((tiempo_oficial) / 42.195)),
    ritmo_5km = hms(seconds = round((parcial_5km) / 5)),
    ritmo_10km = hms(seconds = round((parcial_10km - parcial_5km) / 5)),
    ritmo_15km = hms(seconds = round((parcial_15km - parcial_10km) / 5)),
    ritmo_20km = hms(seconds = round((parcial_20km - parcial_15km) / 5)),
    ritmo_25km = hms(seconds = round((parcial_25km - parcial_20km) / 5)),
    ritmo_30km = hms(seconds = round((parcial_30km - parcial_25km) / 5)),
    ritmo_35km = hms(seconds = round((parcial_35km - parcial_30km) / 5)),
    ritmo_40km = hms(seconds = round((parcial_40km - parcial_35km) / 5))
  )

4.4.2 Categorización por Nivel de Rendimiento

Con el principal objetivo de poder analizar posteriormente de una manera más enriquecida los posibles grupos de carrera, se ha decidido crear una nueva variable que determinará el nivel del atleta.

Para definir los umbrales que diferenciarán dicho nivel se ha atendido primeramente a la marca mínima para participar en el Campeonato del Mundo. Establecida la élite, para la creación de las categorías de “Alto nivel”, “Muy entrenado/a” y “Moderadamente entrenado/a” y “Principiante” se han observado las marcas con que las Majors organizan los cajones de salida.

Los niveles han resultado así:

  • Élite - < 02:17:00 (Men(M)) / < 02:43:00 (Women(W))
  • Alto nivel - < 02:30:00 (Men(M)) / < 03:00:00 (Women(W))
  • Muy entrenado/a - < 03:00:00 (Men(M)) / < 03:30:00 (Women(W))
  • Moderadamente entrenado/a - < 03:30:00 (Men(M)) / < 04:00:00 (Women(W))
  • Principiante - + 03:30:00 (Men(M)) / + 04:00:00 (Women(W))
clasificar_atletas <- function(resultadosTokyo2025, genero_col = Genero, tiempo_col = tiempo_oficial) {
  resultadosTokyo2025 %>%
    mutate(
      Genero_raw = tolower(trimws(as.character({{genero_col}}))),
      Genero = case_when(
        Genero_raw %in% c("women") ~ "F",
        Genero_raw %in% c("men")   ~ "M",
        TRUE ~ NA_character_
      ),
      tiempo_hms = as_hms({{tiempo_col}}),
      categoria = case_when(
        #hombres
        Genero == "M" & tiempo_hms < as_hms("02:17:00") ~ "Élite",
        Genero == "M" & tiempo_hms < as_hms("02:30:00") ~ "Alto nivel",
        Genero == "M" & tiempo_hms < as_hms("03:00:00") ~ "Muy entrenado",
        Genero == "M" & tiempo_hms < as_hms("03:30:00") ~ "Moderadamente entrenado",
        Genero == "M" & tiempo_hms >= as_hms("03:30:00") ~ "Principiante",
        #mujeres
        Genero == "F" & tiempo_hms < as_hms("02:43:00") ~ "Élite",
        Genero == "F" & tiempo_hms < as_hms("03:00:00") ~ "Alto nivel",
        Genero == "F" & tiempo_hms < as_hms("03:30:00") ~ "Muy entrenada",
        Genero == "F" & tiempo_hms < as_hms("04:00:00") ~ "Moderadamente entrenada",
        Genero == "F" & tiempo_hms >= as_hms("04:00:00") ~ "Principiante",
        
        TRUE ~ "No élite"
      ),
      categoria = factor(categoria, levels = c("Élite", "Alto nivel", "Muy entrenado", "Muy entrenada", "Moderadamente entrenado", "Moderadamente entrenada", "Principiante", "No élite"))
    ) %>%
    select(-Genero_raw)
}
resultadosTokyo2025 <- clasificar_atletas(resultadosTokyo2025)

5 Análisis Univariante

Bajo este análisis se examinará cada variable del conjunto de datos para resumir su distribución, principales características y tendencias. Este tipo de análisis se centra en una sola variable a la vez, sin tener en cuenta su relación con otras, y permite obtener una primera descripción general de los datos. A continuación se revisa cada variable del dataset para observar sus principales características estadísticas.

5.1 Edad

Esta variable indica la edad de los corredores que participaron en la maratón. Se trata de una variable numérica discreta. Primero se examinán los principales estadísticos de centralización: media, moda y mediana

# Fun. moda que devuelve el valor más frecuente (si hay empates devuelve el primero)
moda <- function(x) {
  x <- x[!is.na(x)]
  if (length(x) == 0) return(NA_real_)
  ux <- unique(x)
  ux[which.max(tabulate(match(x, ux)))]
}

# Estadísticos resumidos (redondeados)
edad_stats <- resultadosTokyo2025 %>%
  summarise(
    Total_valores = sum(!is.na(Edad)),
    Valores_faltantes = sum(is.na(Edad)),
    Media = round(mean(Edad, na.rm = TRUE), 2),
    Mediana = median(Edad, na.rm = TRUE),
    Moda = moda(Edad),
  )

datatable(
  edad_stats,
  options = list(dom = 't'),
  rownames = FALSE
)

La edad media de los corredores que participaron en la maratón de Tokyo 2025 es de aproximadamente 46 años, con una mediana de 47 años y una moda de 50 años. Esto indica que la distribución de edades está muy ligeramente sesgada hacia edades mayores, ya que la media es menor que la mediana. La moda sugiere que la edad más común entre los corredores es de 50 años, lo que podría indicar una mayor participación de corredores en este rango de edad.

Representado de forma gráfica, en la siguiente figura se observa un histograma con línea de densidad.

ggplot(resultadosTokyo2025, aes(x = Edad)) +
  geom_histogram(aes(y = ..density..), binwidth=2, fill = "#90CAF9", color = "gray30") +
  geom_density(aes(y = ..density..), 
               fill = "#1976D2", alpha = 0.15) +
  labs(title = "Distribución de Edad",
       x = "Edad (años)", y = "Densidad") +
  theme_minimal(base_size = 12)
Warning: The dot-dot notation (`..density..`) was deprecated in ggplot2 3.4.0.
ℹ Please use `after_stat(density)` instead.

En esta gráfica se observa que la distribución de edades de los corredores es aproximadamente normal, con una ligera asimetría positiva (hacia la derecha). La mayoría de los corredores tienen edades comprendidas entre los 25 y 40 años, con un pico alrededor de los 30 años. La media, mediana y moda están bastante cercanas entre sí, lo que indica que la distribución es simétrica.

A continuación se presentan los estadísticos de dispersión y forma de la distribución de edades.

edad_disp_forma <- resultadosTokyo2025 %>%
  summarise(
    Minimo = min(Edad, na.rm = TRUE),
    Maximo = max(Edad, na.rm = TRUE),
    Rango = max(Edad, na.rm = TRUE) - min(Edad, na.rm = TRUE),
    Varianza = round(var(Edad, na.rm = TRUE), 2),
    Desviación_Estandar = round(sd(Edad, na.rm = TRUE), 2),
    Desviacion_tipica = round(sqrt(var(Edad, na.rm = TRUE)), 2),
    Coeficiente_Variacion = round((sd(Edad, na.rm = TRUE) / mean(Edad, na.rm = TRUE)) * 100, 2),
    Asimetría = round(skewness(Edad, na.rm = TRUE), 2),
    Curtosis = round(kurtosis(Edad, na.rm = TRUE), 2)
  )

datatable(
  edad_disp_forma,
  options = list(dom = 't'),
  rownames = FALSE
)

Las edades varían desde un mínimo de 18 años hasta un máximo de 84 años, con un rango total de 65 años. La varianza (122,64) y la desviación estándar (11,07) indican que hay una dispersión moderada en las edades de los corredores. El coeficiente de variación del 23,77% sugiere que la variabilidad relativa de las edades es considerable.

La asimetría negativa (-0,06) indica que la distribución de edades es ligeramente sesgada hacia la izquierda, aunque está muy cerca de cero, lo que sugiere una distribución casi simétrica. La curtosis (-0,58) indica que la distribución de edades es ligeramente más plana que una distribución normal, lo que sugiere que hay menos valores extremos en las edades de los corredores.

En la siguiente grafica se observa un boxplot que muestra la dispersión de las edades de los corredores.

ggplot(resultadosTokyo2025, aes(y = Edad)) +
  geom_boxplot(fill = "#90CAF9", color = "gray30", outlier.color = "red", outlier.size = 1.5) +
  labs(title = "Boxplot de Edad",
       y = "Edad (años)") +
  theme_minimal(base_size = 12)

El boxplot muestra que la mayoría de las edades de los corredores están concentradas entre aproximadamente 39 y 50 años, con algunos valores atípicos en ambos extremos. La mediana (línea dentro de la caja) está cerca del centro de la caja, lo que indica una distribución simétrica de las edades. Los bigotes del boxplot indican que no hay valores extremadamente alejados del rango intercuartílico, lo que sugiere una distribución relativamente homogénea de las edades.

rm(edad_disp_forma, edad_stats)

5.2 Género

En esta sección se realiza un análisis descriptivo de la variable Genero. Para ello, vamos a comenzar inspeccionando las categorías de la variable y sus frecuencias, tanto absoluta (número de corredores) como relativa (porcentaje del total).

tabla_frecuencias_genero <- resultadosTokyo2025 %>%
  count(Genero, name = "Frecuencia_Absoluta") %>%
  mutate(
    Porcentaje = Frecuencia_Absoluta / sum(Frecuencia_Absoluta) * 100
  ) %>%
  arrange(desc(Frecuencia_Absoluta))


kable(
  tabla_frecuencias_genero,
  col.names = c("Género", "Nº de Corredores", "Porcentaje (%)"),
  digits = c(0, 0, 2),
  caption = "Tabla: Distribución de participantes por género."
)
Tabla: Distribución de participantes por género.
Género Nº de Corredores Porcentaje (%)
M 26701 73.83
F 9425 26.06
NA 38 0.11

Se puede observar que existen 3 categorías con la siguiente participación:

  • Men:
  • Women
  • Non-Binary

A continuación vamos a graficar la información obtenida en la tabla de frecuencia a través de un gráfico de barras.

ggplot(tabla_frecuencias_genero, aes(x = reorder(Genero, -Frecuencia_Absoluta), y = Frecuencia_Absoluta, fill = Genero)) +
  geom_bar(stat = "identity", show.legend = FALSE) +
  geom_text(
    aes(label = scales::comma(Frecuencia_Absoluta)), 
    vjust = -0.5, 
    size = 3.5,
    color = "black" 
  ) +
  
  scale_fill_viridis_d() + 
  
  # Títulos, subtítulos y etiquetas de los ejes
  labs(
    title = "Distribución de Participantes por Género",
    subtitle = "Maratón de Tokyo | 02-03-2025",
    x = "Género",
    y = "Número de Corredores"
  ) +
  
  # Tema limpio y ajuste de eje para dar espacio a las etiquetas
  theme_minimal(base_size = 14) + # Aumenté un poco el tamaño base de la letra
  scale_y_continuous(expand = expansion(mult = c(0, 0.1))) +
  theme(
    plot.title = element_text(face = "bold", size = 18), # Título en negrita y más grande
    plot.subtitle = element_text(size = 12, color = "gray30") # Subtítulo más sutil
  )

Por lo tanto podemos observar que la composición de la maratón muestra que casi tres de cada cuatro corredores eran hombres.

rm(tabla_frecuencias_genero)

5.3 Nacionalidad

A continuación, se realizará un análisis univariante de la Nacionalidad de los corredores, para ello, vamos a realizar un mapa interactivo con la librería Plotly en el que se podrá visualizar el número de participantes por país en una escala de colores:

# 1. Contar corredores (asumimos que ya tienes esto)
frecuencia_paises <- resultadosTokyo2025 %>%
count(Pais_Estandarizado, CODE, name = "corredores", sort = TRUE)

# 2. Crear una columna para el logaritmo de corredores
frecuencia_paises <- frecuencia_paises %>%
  mutate(corredores_log = log10(corredores + 1))

# 3. Crear mapa interactivo con la escala logarítmica
fig <- plot_ly(
  frecuencia_paises,
  type = 'choropleth',
  locations = ~CODE,
  
  # Z: La variable que define el color ahora es el logaritmo
  z = ~corredores_log, 
  
  # Text: Mantenemos el nombre del país para el hover
  text = ~Pais_Estandarizado,
  
  # Customdata: Guardamos el número real de corredores para mostrarlo en el hover
  customdata = ~corredores,
  
  # Elige una paleta de colores que resalte bien. "Viridis", "Plasma" o "YlOrRd" son buenas opciones.
  colorscale = "Viridis",
  reversescale = TRUE, # Invertir la escala para que más sea más oscuro
  
  colorbar = list(title = "Corredores (Escala Log)"),
  
  # Hovertemplate: Mostramos el número real de participantes usando 'customdata'
  hovertemplate = "<b>%{text}</b><br>Participantes: %{customdata}<extra></extra>"
) %>%
  layout(
    title = "Distribución de Corredores por País (Maratón de Tokio)",
    geo = list(showframe = FALSE, showcoastlines = FALSE, projection = list(type = 'mercator'))
  )

fig

En este mapa se puede observar que el país que más participantes tuvo fue Japón, seguramente debido a que dicha maratón se realizó en este país. Otros países que llevaron más de 1000 corredores fueron Estados Unidos, Taiwán, China e Inglaterra.

5.4 Tiempo Oficial

En el caso de las variables de timepos, se realizará el análisis con las columnas transformadas a segundos pero reflejando los resultados en formato horas, minutos y segundos para una mejor interpretación. Esta transformación implica que la varible es numérica continua. Como en las variables anteriores, se comenzará con los estadísticos de centralización.

# Estadísticos resumidos (redondeados)
tiempo_stats <- resultadosTokyo2025 %>%
  summarise(
    Total_valores = sum(!is.na(tiempo_oficial)),
    Valores_faltantes = sum(is.na(tiempo_oficial)),
    Media = round(mean(tiempo_oficial, na.rm = TRUE), 2),
    Mediana = median(tiempo_oficial, na.rm = TRUE),
    Moda = moda(tiempo_oficial),
  )

# Convertir a hms para mejor interpretación
tiempo_stats <- tiempo_stats %>%
  mutate(
    Media = as_hms(Media),
    Mediana = as_hms(Mediana),
    Moda = as_hms(Moda)
  )

datatable(
  tiempo_stats,
  options = list(dom = 't'),
  rownames = FALSE
)

La media del tiempo oficial de los corredores que participaron en la maratón de Tokyo 2025 es de aproximadamente 4 horas, 40 minutos y 23 segundos, con una mediana de 4 horas, 36 minutos y 13 segundos. Esto indica que la distribución de los tiempos está sesgada positivamente, ya que la media es mayor que la mediana, es decir, hay corredores con tiempos significativamente más altos que elevan la media. La moda sugiere que el tiempo más común entre los corredores es de 4 horas, 15 minutos y 30 segundos.

Representado de forma gráfica, en la siguiente figura se observa un histograma con línea de densidad.

# Convierte a hms para mejor interpretación

ggplot(resultadosTokyo2025, aes(x = tiempo_oficial)) +
  geom_histogram(aes(y = ..density..), binwidth=500, fill = "#90CAF9", color = "gray30") +
  geom_density(aes(y = ..density..), 
               fill = "#1976D2", alpha = 0.15) +
  scale_x_continuous(
    breaks = seq(0, 21600, by = 3600), # cada 30 minutos
    labels = function(x) as_hms(x)      # convierte a hms para etiquetas
  ) +
  labs(title = "Distribución del Tiempo Oficial",
       x = "Tiempo Oficial (hh:mm:ss)", y = "Densidad") +
  theme_minimal(base_size = 12)

En esta gráfica se observa que la distribución de los tiempos oficiales de los corredores es asimétrica positivamente (hacia la derecha). La mayoría de los corredores tienen tiempos comprendidos entre 3 horas y 30 minutos y 5 horas, con un pico alrededor de las 4 horas.

A continuación se presentan los estadísticos de dispersión y forma de la distribución de tiempos oficiales.

tiempo_disp_forma <- resultadosTokyo2025 %>%
  summarise(
    Minimo = as_hms(min(tiempo_oficial, na.rm = TRUE)),
    Maximo = as_hms(max(tiempo_oficial, na.rm = TRUE)),
    Rango = as_hms(max(tiempo_oficial, na.rm = TRUE) - min(tiempo_oficial, na.rm = TRUE)),
    Varianza = round(var(tiempo_oficial, na.rm = TRUE), 2),
    Desviación_Estandar = round(sd(tiempo_oficial, na.rm = TRUE), 2),
    Coeficiente_Variacion = round((sd(tiempo_oficial, na.rm = TRUE) / mean(tiempo_oficial, na.rm = TRUE)) * 100, 2),
    Asimetría = round(skewness(tiempo_oficial, na.rm = TRUE), 2),
    Curtosis = round(kurtosis(tiempo_oficial, na.rm = TRUE), 2)
  )

datatable(
  tiempo_disp_forma,
  options = list(dom = 't'),
  rownames = FALSE
)

El rango de 4 horas, 56 minutos y 55 segundos indica una amplia variabilidad en los tiempos oficiales de los corredores. La desviación estándar de 3898.73 (~1 hora y 5 minutos) sugiere que los tiempos suelen oscilar alrededor de ±1 h de la media, lo que implica cierta variabilidad, pero no excesiva. El coeficiente de variación del 23.77% indica que la variabilidad relativa de los tiempos oficiales es considerable.

La asimetría positiva (0.15) indica que la distribución de los tiempos oficiales está ligeramente sesgada hacia la derecha, lo que significa que hay más corredores con tiempos superiores a la media. La curtosis (-0.87) indica que la distribución de los tiempos oficiales es más plana que una distribución normal, lo que sugiere que hay menos valores extremos en los tiempos oficiales de los corredores.

5.5 Tiempos Parciales

En cuanto a los tiempos parciales se analiza cada parcial de cada 5km por separado. Primero se presentan los estadísticos de centralización para cada parcial.

# cols_tiempo pero quitando timepo_oficial
tiempos_parciales <- c(
  "parcial_5km","parcial_10km","parcial_15km","parcial_20km","medio_maraton",
  "parcial_25km","parcial_30km","parcial_35km","parcial_40km"
)

# Tabla resumen para cada parcial
parciales_stats <- lapply(tiempos_parciales, function(parcial) {
  resultadosTokyo2025 %>%
    summarise(
      Parcial = parcial,
      Total_valores = sum(!is.na(.data[[parcial]])),
      Valores_faltantes = sum(is.na(.data[[parcial]])),
      Media = round(mean(.data[[parcial]], na.rm = TRUE), 2),
      Mediana = median(.data[[parcial]], na.rm = TRUE),
      Moda = moda(.data[[parcial]])
    ) %>%
    mutate(
      Media = as_hms(Media),
      Mediana = as_hms(Mediana),
      Moda = as_hms(Moda)
    )
})

datatable(
  do.call(rbind, parciales_stats),
  options = list(dom = 't'),
  rownames = FALSE,
  caption = "Tabla: Estadísticos de centralización para cada parcial."
)

Para representar de forma gráfica la distribución de los tiempos parciales, se ha optado por un gráfico de densidad para cada parcial, con una línea discontinua que indica la mediana de cada distribución.

df_plot <- resultadosTokyo2025 %>%
  select(all_of(tiempos_parciales)) %>%
  pivot_longer(everything(), names_to = "parcial", values_to = "segundos") %>%
  filter(!is.na(segundos)) %>%
  mutate(parcial = factor(parcial, levels = tiempos_parciales))

medianas <- df_plot %>%
  group_by(parcial) %>%
  summarise(mediana = median(segundos, na.rm = TRUE), .groups = "drop")

etiquetas <- setNames(
  c("Parcial 5km","Parcial 10km","Parcial 15km","Parcial 20km","Medio maratón",
    "Parcial 25km","Parcial 30km","Parcial 35km","Parcial 40km"),
  tiempos_parciales
)

p_density <- ggplot(df_plot, aes(x = segundos)) +
  geom_density(fill = "#90CAF9", alpha = 0.6, na.rm = TRUE, linewidth = 0.4) +
  geom_vline(data = medianas, aes(xintercept = mediana),
             color = "#37474F", linetype = "dashed", linewidth = 0.6) +
  facet_wrap(~parcial, scales = "free_x", ncol = 3, labeller = labeller(parcial = etiquetas)) +
  scale_x_continuous(
    breaks = function(x) pretty(x, n = 3),                       # pocas marcas por facet
    labels = function(x) format(as_hms(x), "%H:%M"),             # mostrar hh:mm
    expand = expansion(mult = c(0.03, 0.03))
  ) +
  scale_y_continuous(expand = expansion(mult = c(0, 0.05))) +
  labs(
    title = "Densidad de tiempos por parcial",
    x = "Tiempo (hh:mm)",
    y = "Densidad"
  ) +
  theme_minimal(base_size = 11) +
  theme(
    strip.text = element_text(face = "bold"),
    panel.spacing = unit(1, "lines"),
    panel.border = element_rect(color = "gray80", fill = NA, linewidth = 0.5),
    strip.background = element_rect(fill = "#ECEFF1", color = NA),
    axis.title.x = element_text(margin = margin(t = 6)),
    axis.text.x = element_text(angle = 45, hjust = 1, vjust = 1), # rota etiquetas
    plot.title = element_text(face = "bold")
  )

p_density

Se puede observar que a medida que avanza la carrera, la distribución de los tiempos parciales tiende a desplazarse hacia la derecha, indicando que los corredores tardan más en completar cada segmento a medida que avanza la maratón. Además, las distribuciones parecen volverse más anchas en los parciales posteriores, lo que sugiere una mayor variabilidad en los tiempos a medida que los corredores avanzan en la carrera y se fatigan.

En cuanto a las principales medidas de dispersión en cada parcial, se presentan a continuación:

# Tabla resumen para cada parcial
parciales_disp_forma <- lapply(tiempos_parciales, function(parcial) {
  resultadosTokyo2025 %>%
    summarise(
      Parcial = parcial,
      Minimo = as_hms(min(.data[[parcial]], na.rm = TRUE)),
      Maximo = as_hms(max(.data[[parcial]], na.rm = TRUE)),
      Rango = as_hms(max(.data[[parcial]], na.rm = TRUE) - min(.data[[parcial]], na.rm = TRUE)),
      Varianza = round(var(.data[[parcial]], na.rm = TRUE), 2),
      Desviación_Estandar = round(sd(.data[[parcial]], na.rm = TRUE), 2),
      Coeficiente_Variacion = round((sd(.data[[parcial]], na.rm = TRUE) / mean(.data[[parcial]], na.rm = TRUE)) * 100, 2),
      Asimetría = round(skewness(.data[[parcial]], na.rm = TRUE), 2),
      Curtosis = round(kurtosis(.data[[parcial]], na.rm = TRUE), 2)
    )
})

datatable(
  do.call(rbind, parciales_disp_forma),
  options = list(dom = 't'),
  rownames = FALSE,
  caption = "Tabla: Estadísticos de dispersión y forma para cada parcial."
)

De forma gráfica cada boxplot representa un parcial de la maratón.

# Boxplots para cada parcial
ggplot(df_plot, aes(y = segundos, x = parcial)) +
  geom_boxplot(fill = "#90CAF9", color = "gray30", outlier.color = "red", outlier.size = 1.5) +
  scale_x_discrete(
    labels = etiquetas
  ) +
  scale_y_continuous(
    labels = function(x) format(as_hms(x), "%H:%M"),
    breaks = seq(0, 21600, by = 1800)
  ) +
  labs(
    title = "Boxplots de tiempos por parcial",
    x = "Parcial",
    y = "Tiempo (hh:mm:ss)"
  ) +
  theme_minimal(base_size = 11) +
  theme(
    axis.text.x = element_text(angle = 45, hjust = 1),
    plot.title = element_text(face = "bold")
  )

Estos boxplots muestran la distribución de los tiempos parciales para cada segmento de la maratón. Se observa que a medida que avanza la carrera, los tiempos tienden a aumentar, lo que es consistente con la fatiga acumulada de los corredores. Además, la variabilidad en los tiempos también parece aumentar en los parciales posteriores, lo que sugiere que algunos corredores logran mantener un ritmo más constante mientras que otros experimentan una mayor desaceleración.

5.6 Ritmos

En el caso de los ritmos parciales, el análisis es practicamente idéntico al de los tiempos parciales, ya que son variables derivadas directamente de estos. Por lo tanto, se presentan directamente los boxplots para cada ritmo parcial.

# cols_ritmo
ritmos_parciales <- c(
  "ritmo_5km","ritmo_10km","ritmo_15km","ritmo_20km",
  "ritmo_25km","ritmo_30km","ritmo_35km","ritmo_40km"
)

# Boxplots para cada ritmo parcial
df_ritmos <- resultadosTokyo2025 %>%
  select(all_of(ritmos_parciales)) %>%
  pivot_longer(everything(), names_to = "ritmo", values_to = "segundos") %>%
  filter(!is.na(segundos)) %>%
  mutate(ritmo = factor(ritmo, levels = ritmos_parciales))

etiquetas_ritmo <- setNames(
  c("Ritmo 5km","Ritmo 10km","Ritmo 15km","Ritmo 20km",
    "Ritmo 25km","Ritmo 30km","Ritmo 35km","Ritmo 40km"),
  ritmos_parciales
)

ggplot(df_ritmos, aes(y = segundos, x = ritmo)) +
  geom_boxplot(fill = "#90CAF9", color = "gray30", outlier.color = "red", outlier.size = 1.5) +
  scale_x_discrete(
    labels = etiquetas_ritmo
  ) +
  scale_y_continuous(
    labels = function(x) format(as_hms(x), "%M:%S"),
    breaks = seq(0, 900, by = 60)
  ) +
  labs(
    title = "Boxplots de ritmos por parcial",
    x = "Parcial",
    y = "Ritmo (mm:ss por km)"
  ) +
  theme_minimal(base_size = 11) +
  theme(
    axis.text.x = element_text(angle = 45, hjust = 1),
    plot.title = element_text(face = "bold")
  )

clasificar_atletas_fem <- function(resultadosTokyo2025, genero_col = Genero, tiempo_col = tiempo_oficial) {
  resultadosTokyo2025 %>%
    mutate(
      Genero_raw = tolower(trimws(as.character({{genero_col}}))),
      Genero = case_when(
        Genero_raw %in% c("women") ~ "F",
        Genero_raw %in% c("men")   ~ "M",
        TRUE ~ NA_character_
      ),
      tiempo_hms = as_hms({{tiempo_col}}),
      categoria = case_when(
        #mujeres
        Genero == "F" & tiempo_hms < as_hms("02:43:00") ~ "Élite",
        Genero == "F" & tiempo_hms < as_hms("03:00:00") ~ "Alto nivel",
        Genero == "F" & tiempo_hms < as_hms("03:30:00") ~ "Muy entrenada",
        Genero == "F" & tiempo_hms < as_hms("04:00:00") ~ "Moderadamente entrenada",
        Genero == "F" & tiempo_hms >= as_hms("04:00:00") ~ "Principiante",
      )
    ) %>%
    select(-Genero_raw)
}
resultados_clasificados_masc <- clasificar_atletas_masc(resultadosTokyo2025)
head(resultados_clasificados_masc)
  BIB                   Nombre Nacionalidad Genero Edad tiempo_oficial
1   5            TADESE TAKELE     ETHIOPIA      M   22           7403
2   3            DERESA GELETA     ETHIOPIA      M   29           7431
3   4 VINCENT KIPKEMOI NGETICH        KENYA      M   26           7440
4  12            TITUS KIPRUTO        KENYA      M   26           7534
5  13       MULUGETA ASEFA UMA     ETHIOPIA      M   26           7546
6  16       GEOFFREY TOROITICH        KENYA      M   25           7546
  parcial_5km parcial_10km parcial_15km parcial_20km medio_maraton parcial_25km
1         865         1736         2610         3487          3678         4364
2         865         1735         2610         3487          3678         4364
3         865         1736         2610         3487          3682         4363
4         865         1736         2611         3488          3679         4365
5         880         1763         2650         3535          3728         4416
6         888         1780         2671         3568          3763         4456
  parcial_30km parcial_35km parcial_40km Pais_Estandarizado CODE ritmo_oficial
1         5242         6133         7019           Ethiopia  ETH      00:02:55
2         5243         6133         7030           Ethiopia  ETH      00:02:56
3         5242         6133         7036              Kenya  KEN      00:02:56
4         5255         6193         7127              Kenya  KEN      00:02:59
5         5316         6225         7145           Ethiopia  ETH      00:02:59
6         5352         6264         7166              Kenya  KEN      00:02:59
  ritmo_5km ritmo_10km ritmo_15km ritmo_20km ritmo_25km ritmo_30km ritmo_35km
1  00:02:53   00:02:54   00:02:55   00:02:55   00:02:55   00:02:56   00:02:58
2  00:02:53   00:02:54   00:02:55   00:02:55   00:02:55   00:02:56   00:02:58
3  00:02:53   00:02:54   00:02:55   00:02:55   00:02:55   00:02:56   00:02:58
4  00:02:53   00:02:54   00:02:55   00:02:55   00:02:55   00:02:58   00:03:08
5  00:02:56   00:02:57   00:02:57   00:02:57   00:02:56   00:03:00   00:03:02
6  00:02:58   00:02:58   00:02:58   00:02:59   00:02:58   00:02:59   00:03:02
  ritmo_40km Categoria tiempo_hms categoria
1   00:02:57     Elite   02:03:23     Élite
2   00:02:59     Elite   02:03:51     Élite
3   00:03:01     Elite   02:04:00     Élite
4   00:03:07     Elite   02:05:34     Élite
5   00:03:04     Elite   02:05:46     Élite
6   00:03:00     Elite   02:05:46     Élite

Se puede observar que los ritmos parciales tienden a aumentar (es decir, los corredores corren más lentamente) a medida que avanza la maratón. Además existen mas valores atípicos en los ritmos de los parciales posteriores, lo que indica que algunos corredores tienen dificultades significativas para mantener su ritmo a medida que avanza la carrera.

Y en cuanto al ritmo oficial.

resultados_clasificados_fem <- clasificar_atletas_fem(resultadosTokyo2025)
head(resultados_clasificados_fem)
  BIB                   Nombre Nacionalidad Genero Edad tiempo_oficial
1   5            TADESE TAKELE     ETHIOPIA      M   22           7403
2   3            DERESA GELETA     ETHIOPIA      M   29           7431
3   4 VINCENT KIPKEMOI NGETICH        KENYA      M   26           7440
4  12            TITUS KIPRUTO        KENYA      M   26           7534
5  13       MULUGETA ASEFA UMA     ETHIOPIA      M   26           7546
6  16       GEOFFREY TOROITICH        KENYA      M   25           7546
  parcial_5km parcial_10km parcial_15km parcial_20km medio_maraton parcial_25km
1         865         1736         2610         3487          3678         4364
2         865         1735         2610         3487          3678         4364
3         865         1736         2610         3487          3682         4363
4         865         1736         2611         3488          3679         4365
5         880         1763         2650         3535          3728         4416
6         888         1780         2671         3568          3763         4456
  parcial_30km parcial_35km parcial_40km Pais_Estandarizado CODE ritmo_oficial
1         5242         6133         7019           Ethiopia  ETH      00:02:55
2         5243         6133         7030           Ethiopia  ETH      00:02:56
3         5242         6133         7036              Kenya  KEN      00:02:56
4         5255         6193         7127              Kenya  KEN      00:02:59
5         5316         6225         7145           Ethiopia  ETH      00:02:59
6         5352         6264         7166              Kenya  KEN      00:02:59
  ritmo_5km ritmo_10km ritmo_15km ritmo_20km ritmo_25km ritmo_30km ritmo_35km
1  00:02:53   00:02:54   00:02:55   00:02:55   00:02:55   00:02:56   00:02:58
2  00:02:53   00:02:54   00:02:55   00:02:55   00:02:55   00:02:56   00:02:58
3  00:02:53   00:02:54   00:02:55   00:02:55   00:02:55   00:02:56   00:02:58
4  00:02:53   00:02:54   00:02:55   00:02:55   00:02:55   00:02:58   00:03:08
5  00:02:56   00:02:57   00:02:57   00:02:57   00:02:56   00:03:00   00:03:02
6  00:02:58   00:02:58   00:02:58   00:02:59   00:02:58   00:02:59   00:03:02
  ritmo_40km Categoria tiempo_hms categoria
1   00:02:57     Elite   02:03:23      <NA>
2   00:02:59     Elite   02:03:51      <NA>
3   00:03:01     Elite   02:04:00      <NA>
4   00:03:07     Elite   02:05:34      <NA>
5   00:03:04     Elite   02:05:46      <NA>
6   00:03:00     Elite   02:05:46      <NA>

6 Análisis Bivariante y Multivariante

Tabla de doble entrada para edad y tiempo oficial

tabla_doble_masc <- table(resultados_clasificados_masc$Edad, resultados_clasificados_masc$categoria)
tabla_doble_fem <- table(resultados_clasificados_fem$Edad, resultados_clasificados_fem$categoria)

En el siguiente gráfico se puede ver la relación entre los tiempo finales de los participantes y el genero de los mismos.

# Boxplot tiempo oficial vs genero

ggplot(resultadosTokyo2025, aes(x = Genero, y = tiempo_oficial, fill = Genero)) +
  geom_boxplot(outlier.color = "red", outlier.size = 1.5) +
  scale_y_continuous(
    labels = function(x) as_hms(x),
    breaks = seq(0, 21600, by = 1800)
  ) +
  labs(
    title = "Tiempo Oficial por Género",
    x = "Género",
    y = "Tiempo Oficial (hh:mm:ss)"
  ) +
  theme_minimal(base_size = 12) +
  theme(
    plot.title = element_text(face = "bold", size = 16),
    axis.text.x = element_text(angle = 45, hjust = 1),
    legend.position = "none"
  )

Se observa como los hombres tienden a tener tiempos oficiales más bajos que las mujeres y los corredores no binarios. Hay que tener en cuenta que para los corredores no binarios hay muy pocos datos, por lo que la interpretación de su boxplot puede no ser representativa.

6.2 Tiempo oficial y edad

En el gráfico que sigue se observa la relación entre los tiempos oficiales de los corredores y su edad.

# Boxplot edad vs genero
ggplot(resultadosTokyo2025, aes(x = Genero, y = Edad, fill = Genero)) +
  geom_boxplot(outlier.color = "red", outlier.size = 1.5) +
  labs(
    title = "Edad de los Corredores por Género",
    x = "Género",
    y = "Edad (años)"
  ) +
  theme_minimal(base_size = 12) +
  theme(
    plot.title = element_text(face = "bold", size = 16),
    axis.text.x = element_text(angle = 45, hjust = 1),
    legend.position = "none"
  )

Se observa que la mediana de edad es similar entre hombres y mujeres, aunque los hombres parecen tener una mayor variabilidad en sus edades. Los corredores no binarios tienen una mediana de edad más baja, pero nuevamente, el número de datos es muy limitado para hacer una interpretación sólida.

En cuanto a la nacionalidad, vamos a ver los tiempos oficiales medios por país para los países con al menos 5 corredores.

# preparar df con número de corredores por país
freq_paises <- resultadosTokyo2025 %>%
  count(Pais_Estandarizado, name = "n_corredores")

# preparar df con tiempos medios por país
tiempos_pais <- resultadosTokyo2025 %>%
  group_by(Pais_Estandarizado) %>%
  summarise(
    Tiempo_Medio = as_hms(mean(tiempo_oficial, na.rm = TRUE)),
    .groups = "drop"
)

plot_df <- tiempos_pais %>%
  left_join(freq_paises, by = "Pais_Estandarizado") %>%
  mutate(
    n_corredores = ifelse(is.na(n_corredores), 0L, n_corredores),
    Tiempo_Medio_num = as.numeric(Tiempo_Medio),
    Tiempo_Medio_posix = as.POSIXct(Tiempo_Medio_num, origin = "1970-01-01", tz = "UTC"),
    Tiempo_Medio_label = format(as_hms(Tiempo_Medio_num), "%H:%M")
  )

# Plotly interactivo: barra horizontal + hover con tiempo y nº corredores
plot_ly(
  plot_df,
  x = ~Tiempo_Medio_posix,
  y = ~reorder(Pais_Estandarizado, Tiempo_Medio_posix),
  type = "bar",
  orientation = "h",
  marker = list(color = ~Tiempo_Medio_num, colorscale = "Plasma", reversescale = TRUE),
  text = ~paste0(Pais_Estandarizado, "<br>Tiempo medio: ", Tiempo_Medio_label, "<br>N: ", n_corredores),
  hovertemplate = "%{text}<extra></extra>"
) %>%
  layout(
    title = "Tiempo Oficial Medio por País (interactivo)",
    xaxis = list(tickformat = "%H:%M"),
    margin = list(l = 220)
  )
Warning: Ignoring 1 observations

Es importante tener en cuenta el número de corredores, ya que los países con pocos corredores pueden tener tiempos medios menos representativos.

6.4 Género y edad

Ahora vamos a relacionar la edad con el genero de los corredores para ver si existe alguna diferencia en la edad de los participantes según su género.

# Boxplot edad vs genero
ggplot(resultadosTokyo2025, aes(x = Genero, y = Edad, fill = Genero)) +
  geom_boxplot(outlier.color = "red", outlier.size = 1.5) +
  labs(
    title = "Edad de los Corredores por Género",
    x = "Género",
    y = "Edad (años)"
  ) +
  theme_minimal(base_size = 12) +
  theme(
    plot.title = element_text(face = "bold", size = 16),
    axis.text.x = element_text(angle = 45, hjust = 1),
    legend.position = "none"
  )

Se observa que la mediana de edad es similar entre hombres y mujeres, aunque los hombres parecen tener una mayor variabilidad en sus edades. Los corredores no binarios tienen una mediana de edad más baja, pero nuevamente, el número de datos es muy limitado para hacer una interpretación sólida.

  # ---- Función auxiliar para formatear segundos a "HH:MM" ----
sec_to_hhmm <- function(x) {
  # x en segundos; usamos POSIXct con origin para formatear a HH:MM
  format(as.POSIXct(x, origin = "1970-01-01", tz = "UTC"), "%H:%M:%S")
}


p_smooth <- ggplot(resultadosTokyo2025, aes(x = Edad, y = tiempo_oficial, color = Genero)) +
  geom_point(data = resultadosTokyo2025 %>% sample_n(3000), aes(alpha = 0.3), size = 0.8, show.legend = FALSE) + # muestra una muestra
  geom_smooth(method = "loess", se = TRUE, linewidth = 1) +
  scale_y_continuous(labels = sec_to_hhmm) +
  labs(title = "Tendencia Edad vs Tiempo (LOESS) — muestra + linea suavizada") +
  theme_minimal()
p_smooth
`geom_smooth()` using formula = 'y ~ x'

6.5 Definición de estrategias

Al conocer los tiempos de paso cada 5 kilómetros se han podido obtener los ritmos en min/km de cada parcial. Siguiendo la evolución de estos ritmos parciales se pueden definir varios tipos de estrategias:

  • Positiva: a medida que avanza la carrera el ritmo aumenta
  • Negativa: a medida que avanza la carrera el ritmo disminuye
  • “Even pace”: el ritmo es igual durante toda la carrera
  • Parabólica: los primeros y últimos kilómetros son más rápidos que los centrales
tolerancia <- 8  # margen en segundos

clasificar_estrategia <- function(df) {
  
  # Seleccionar SOLO las columnas ritmo_* pero excluyendo ritmo_oficial
  parciales <- df[grep("^ritmo_", names(df), value = TRUE)]
  parciales <- parciales[!grepl("ritmo_oficial", names(parciales))]
  
  # Convertir a numérico (segundos)
  parciales <- as.numeric(parciales)
  parciales <- parciales[!is.na(parciales)]
  
  if (length(parciales) < 3) return(NA_character_)
  
  # Calcular diferencias y tendencias
  delta_total <- last(parciales) - first(parciales)
  diferencias <- diff(parciales)
  
  # Detectar patrón parabólico
  mitad <- ceiling(length(parciales) / 2)
  ritmo_inicio <- mean(parciales[1:2], na.rm = TRUE)
  ritmo_medio  <- parciales[mitad]
  ritmo_final  <- mean(tail(parciales, 2), na.rm = TRUE)
  
  es_parabolico <- isTRUE(
    (ritmo_medio - ritmo_inicio > tolerancia) &
    (ritmo_medio - ritmo_final > tolerancia)
  )
  
  # Clasificación
  if (isTRUE(es_parabolico)) {
    estrategia <- "Parabólica"
  } else if (all(abs(diferencias) < tolerancia, na.rm = TRUE)) {
    estrategia <- "Uniforme"
  } else if (!is.na(delta_total) && delta_total < -tolerancia) {
    estrategia <- "Negativa"  # termina más rápido
  } else if (!is.na(delta_total) && delta_total > tolerancia) {
    estrategia <- "Positiva"  # termina más lento
  } else {
    estrategia <- "Variable"
  }
  
  return(estrategia)
}

# Aplicar al dataset
resultadosTokyo2025 <- resultadosTokyo2025 %>%
  rowwise() %>%
  mutate(estrategia = clasificar_estrategia(pick(everything()))) %>%
  ungroup()

Nota La estrategia variable no es ninguna estrategia en concreto, hace referencia a los corredores que adaptan su ritmo en cada parcial según sus sensaciones del momento, aumentándolo o disminuyéndolo en función de la fatiga percibida.

6.6 Edad y estrategia

# Calcular conteos por grupo de edad y estrategia
estrategias_edad <- resultadosTokyo2025 %>%
  group_by(grupo_edad, estrategia) %>%
  summarise(n = n(), .groups = "drop")

# Gráfico con colores aptos para daltónicos (Okabe–Ito)
ggplot(estrategias_edad, aes(x = grupo_edad, y = n, fill = estrategia)) +
  geom_col(position = "dodge") +
  scale_fill_manual(
    values = c(
      "#E69F00", # naranja
      "#56B4E9", # azul claro
      "#009E73", # verde
      "#F0E442", # amarillo
      "#0072B2", # azul oscuro
      "#D55E00", # rojo anaranjado
      "#CC79A7"  # rosado
    )
  ) +
  labs(
    title = "Distribución de estrategias por grupo de edad",
    x = "Grupo de edad",
    y = "Número de corredores",
    fill = "Estrategia"
  ) +
  theme_minimal(base_size = 12) +
  theme(
    plot.title = element_text(face = "bold", size = 15),
    axis.text.x = element_text(angle = 45, hjust = 1),
    legend.position = "bottom"
  )

A simple vista, puede comprobarse que independientemente del grupo de edad, la mayoría de corredores siguieron una estrategia positiva, es decir, su ritmo fue disminuyendo a lo largo de la carrera, terminando los últimos kilómetros a un ritmo más lento que los primeros.

Las tácticas de carrera negativa y parabólica fueron las menos empleadas para todos los grupos de edad. En cuanto a la estrategia uniforme, los grupos de edad en los que más corredores consiguieron mantener unos ritmos similares en todos los parciales fueron los comprendidos entre 40 y 44 y entre 45 y 49.

6.7 Género y estrategia

ggplot(resultadosTokyo2025, aes(x = Genero, fill = estrategia)) +
  geom_bar(position = "fill") +
  labs(
    title = "Proporción de estrategias por género",
    x = "Género",
    y = "Proporción dentro del género",
    fill = "Estrategia"
  ) +
  scale_y_continuous(labels = scales::percent) +
  scale_fill_manual(
    values = c(
      "#E69F00", # naranja
      "#56B4E9", # azul claro
      "#009E73", # verde
      "#F0E442", # amarillo
      "#0072B2", # azul oscuro
      "#D55E00", # rojo anaranjado
      "#CC79A7"  # rosado
    )
  ) +
  theme_minimal(base_size = 14) +
  theme(
    plot.title = element_text(face = "bold", size = 16),
    axis.text.x = element_text(face = "bold"),
    legend.position = "bottom"
  )

Se puede observar que las proporciones de cada estrategia en los corredores masculinos es prácticamente igual en las corredoras femeninas.

6.8 Nivel del atleta y estrategia

colores_okabe_ito <- c(
  "#E69F00", # naranja
  "#56B4E9", # azul claro
  "#009E73", # verde
  "#F0E442", # amarillo
  "#0072B2", # azul oscuro
  "#D55E00", # rojo anaranjado
  "#CC79A7"  # rosado
)

# --- HOMBRES ---
ggplot(resultadosTokyo2025 %>% filter(Genero == "M"),
       aes(x = categoria, fill = estrategia)) +
  geom_bar(position = "fill") +  # proporciones
  scale_y_continuous(labels = scales::percent) +
  scale_fill_manual(values = colores_okabe_ito) +
  labs(
    title = "Estrategias de carrera según nivel (Hombres)",
    x = "Nivel del atleta",
    y = "Proporción",
    fill = "Estrategia"
  ) +
  theme_minimal(base_size = 14) +
  theme(
    axis.text.x = element_text(angle = 45, hjust = 1),
    plot.title = element_text(face = "bold", size = 16),
    legend.position = "bottom"
  )

# --- MUJERES ---
ggplot(resultadosTokyo2025 %>% filter(Genero == "F"),
       aes(x = categoria, fill = estrategia)) +
  geom_bar(position = "fill") +
  scale_y_continuous(labels = scales::percent) +
  scale_fill_manual(values = colores_okabe_ito) +
  labs(
    title = "Estrategias de carrera según nivel (Mujeres)",
    x = "Nivel del atleta",
    y = "Proporción",
    fill = "Estrategia"
  ) +
  theme_minimal(base_size = 14) +
  theme(
    axis.text.x = element_text(angle = 45, hjust = 1),
    plot.title = element_text(face = "bold", size = 16),
    legend.position = "bottom"
  )

Estas dos gráficas ilustran la principal diferencia entre el nivel de los atletas más allá del resultado objetivo marcado por el tiempo oficial. La proporción de atletas que optaron por una táctica uniforme fue de aproximadamente un 50% en hombres y cerca de un 60% en mujeres. En los atletas de alto nivel, la proporción fue de alrededor de un 40% en hombres y un 50% en mujeres. Tanto para el sexo masculino como para el femenino, la proporción de atletas muy entrenados que llevaron una táctica uniforme está entre el 20% y el 25%. La proporción más alta de atletas que optaron por una estrategia negativa (recorrer los últimos kilómetros más rápido que los primeros) se ha dado en atletas masculinos y femeninos de nivel “moderamente entrenado”.

7 Identificación de Patrones y Formulación de Preguntas

Un patrón llamativo que se ha estudiado ha sido el comportamiento del tiempo oficial de los participantes. Su distribución era asimétrica y desplazada hacia la derecha, lo que quiere decir que había muy pocos corredores con marcas de élite y muchos corredores con marcas más cercanas a la media. Si se estudiaran otras “Major Marathon” se podría comparar la distribución de los tiempos en cada una de ellas y considerar la viabilidad y utilidad de establecer perfiles de maratón. Por otro lado, sería posible comparar la distribución de una “Major Marathon” con otros formatos de competición como Campeonatos Continentales, Mundiales, Juegos Olímpicos o incluso maratones enteramente populares. Dichas comparaciones podrían ser de utilidad para entrenadores y/o managers deportivos.

Otro hecho llamativo es el desplazamiento hacia la derecha de la distribución de los tiempos parciales. Algunas preguntas que pueden surgir para análisis futuros son:

  1. ¿En qué momento o momentos hay un cambio notable en el ritmo de los atletas?

  2. ¿Hay algún cambio que sea común a todos los atletas independientemente del nivel? Esto puede indicar irregularidad del terreno, cambio en los elementos ambientales, mayor o menor presencia de público, entre otros factores externos.

  3. ¿Qué factores psicofisiológicos están detrás de los cambios de ritmo? Esto permitiría plantear las planificaciones de entrenamiento teniendo en cuenta el punto crítico en que el organismo se encuentra en condiciones especialmente hostiles.

La carrera en números:

  1. ¿Cuántos atletas bajaron de 2h10? ¿2h30? ¿3h00?

  2. ¿Qué porcentaje de atletas llevó x ritmo?

  3. ¿A cuánto tiempo y porcentaje de tiempo se quedaron los mejores tiempos masculino y femenino del récord del mundo?

  4. ¿Qué nacionalidad tuvo el mejor rendimiento?

  5. ¿Alguna marca entra en el top10 histórico?

  6. ¿Cuál ha sido el lapso de tiempo al entrar en meta entre los tres primeros?

  7. ¿Cuántos atletas olímpicos/internacionales han participado?

7.1 Conclusiones